From 6e3e2d9f29e48513239d2e2c4912737b10e17f37 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Florian=20M=C3=BCllner?= Date: Thu, 31 Jan 2013 17:13:37 +0100 Subject: [PATCH] appDisplay: Add new FolderIcon class FolderIcons will appear in the primary app view along AppIcons, but represent a group of applications rather than a single application. https://bugzilla.gnome.org/show_bug.cgi?id=694192 --- data/theme/gnome-shell.css | 21 ++++ js/ui/appDisplay.js | 194 +++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 41f9965da..e05d83a9c 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -857,6 +857,10 @@ StScrollBar StButton#vhandle:active { padding-right: 32px; } +.app-folder-icon { + padding: 5px; +} + .dash-item-container > StButton { padding: 4px 8px; } @@ -896,12 +900,27 @@ StScrollBar StButton#vhandle:active { text-align: center; } +.app-folder-popup { + -arrow-border-radius: 8px; + -arrow-background-color: black; + -arrow-base: 24px; + -arrow-rise: 11px; +} + +.app-folder-popup-bin { + padding: 15px; +} + .app-well-app.running > .overview-icon { text-shadow: black 0px 2px 2px; background-image: url("running-indicator.svg"); background-size: contain; } +.app-well-app.app-folder > .overview-icon { + background-color: rgba(0,0,0,0.5); +} + .app-well-app:hover > .overview-icon, .show-apps:hover > .overview-icon, .search-provider-icon:hover, @@ -925,6 +944,8 @@ StScrollBar StButton#vhandle:active { color: white; } +.app-well-app:checked > .overview-icon, +.app-well-app:active > .overview-icon, .show-apps:checked > .overview-icon, .show-apps:active > .overview-icon { background-gradient-start: rgba(255, 255, 255, .05); diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 457bcdc4d..cc4a665a9 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -3,6 +3,7 @@ const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const GMenu = imports.gi.GMenu; const Shell = imports.gi.Shell; @@ -14,6 +15,7 @@ const Mainloop = imports.mainloop; const Atk = imports.gi.Atk; const AppFavorites = imports.ui.appFavorites; +const BoxPointer = imports.ui.boxpointer; const DND = imports.ui.dnd; const IconGrid = imports.ui.iconGrid; const Main = imports.ui.main; @@ -29,6 +31,9 @@ const MENU_POPUP_TIMEOUT = 600; const SCROLL_TIME = 0.1; const MAX_COLUMNS = 6; +const FOLDER_SUBICON_FRACTION = .4; + + // Recursively load a GMenuTreeDirectory; we could put this in ShellAppSystem too function _loadCategory(dir, view) { let iter = dir.iter(); @@ -93,6 +98,51 @@ const AlphabeticalView = new Lang.Class({ } }); +const FolderView = new Lang.Class({ + Name: 'FolderView', + Extends: AlphabeticalView, + + _init: function() { + this.parent(); + this.actor = this._grid.actor; + }, + + _getItemId: function(item) { + return item.get_id(); + }, + + _createItemIcon: function(item) { + return new AppIcon(item); + }, + + _compareItems: function(a, b) { + return a.compare_by_name(b); + }, + + addApp: function(app) { + this._addItem(app); + }, + + createFolderIcon: function(size) { + let icon = new St.Widget({ layout_manager: new Clutter.BinLayout(), + style_class: 'app-folder-icon', + width: size, height: size }); + let subSize = Math.floor(FOLDER_SUBICON_FRACTION * size); + + let aligns = [ Clutter.ActorAlign.START, Clutter.ActorAlign.END ]; + for (let i = 0; i < Math.min(this._allItems.length, 4); i++) { + let texture = this._allItems[i].create_icon_texture(subSize); + let bin = new St.Bin({ child: texture, + x_expand: true, y_expand: true }); + bin.set_x_align(aligns[i % 2]); + bin.set_y_align(aligns[Math.floor(i / 2)]); + icon.add_actor(bin); + } + + return icon; + } +}); + const AllView = new Lang.Class({ Name: 'AllView', Extends: AlphabeticalView, @@ -145,6 +195,11 @@ const AllView = new Lang.Class({ addFolderPopup: function(popup) { this._stack.add_actor(popup.actor); + popup.connect('open-state-changed', Lang.bind(this, + function(popup, isOpen) { + if (isOpen) + this._ensureIconVisible(popup.actor); + })); }, _ensureIconVisible: function(icon) { @@ -288,6 +343,145 @@ const AppSearchProvider = new Lang.Class({ } }); +const FolderIcon = new Lang.Class({ + Name: 'FolderIcon', + + _init: function(dir, parentView) { + this._dir = dir; + this._parentView = parentView; + + this.actor = new St.Button({ style_class: 'app-well-app app-folder', + button_mask: St.ButtonMask.ONE, + toggle_mode: true, + can_focus: true, + x_fill: true, + y_fill: true }); + this.actor._delegate = this; + + let label = this._dir.get_name(); + this.icon = new IconGrid.BaseIcon(label, + { createIcon: Lang.bind(this, this._createIcon) }); + this.actor.set_child(this.icon.actor); + this.actor.label_actor = this.icon.label; + + this.view = new FolderView(); + this.view.actor.reactive = false; + _loadCategory(dir, this.view); + + this.actor.connect('clicked', Lang.bind(this, + function() { + this._ensurePopup(); + this._popup.toggle(); + })); + this.actor.connect('notify::mapped', Lang.bind(this, + function() { + if (!this.actor.mapped && this._popup) + this._popup.popdown(); + })); + }, + + _createIcon: function(size) { + return this.view.createFolderIcon(size); + }, + + _ensurePopup: function() { + if (this._popup) + return; + + let spaceTop = this.actor.y; + let spaceBottom = this._parentView.actor.height - (this.actor.y + this.actor.height); + let side = spaceTop > spaceBottom ? St.Side.BOTTOM : St.Side.TOP; + + this._popup = new AppFolderPopup(this, side); + this._parentView.addFolderPopup(this._popup); + let constraint = new Clutter.AlignConstraint({ source: this._parentView.actor, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5 }); + this._popup.actor.add_constraint(constraint); + + // Position the popup above or below the source icon + if (side == St.Side.BOTTOM) { + this._popup.actor.show(); + this._popup.actor.y = this.actor.y - this._popup.actor.height; + this._popup.actor.hide(); + } else { + this._popup.actor.y = this.actor.y + this.actor.height; + } + + this._popup.connect('open-state-changed', Lang.bind(this, + function(popup, isOpen) { + if (!isOpen) + this.actor.checked = false; + })); + }, +}); + +const AppFolderPopup = new Lang.Class({ + Name: 'AppFolderPopup', + + _init: function(source, side) { + this._source = source; + this._view = source.view; + this._arrowSide = side; + + this._isOpen = false; + + this.actor = new St.Widget({ layout_manager: new Clutter.BinLayout(), + visible: false }); + this._boxPointer = new BoxPointer.BoxPointer(this._arrowSide, + { style_class: 'app-folder-popup-bin', + x_fill: true, + y_fill: true, + x_align: St.Align.START }); + + this._boxPointer.actor.style_class = 'app-folder-popup'; + this.actor.add_actor(this._boxPointer.actor); + this._boxPointer.bin.set_child(this._view.actor); + + let closeButton = Util.makeCloseButton(); + closeButton.connect('clicked', Lang.bind(this, this.popdown)); + this.actor.add_actor(closeButton); + + this._boxPointer.actor.bind_property('opacity', closeButton, 'opacity', + GObject.BindingFlags.SYNC_CREATE); + + source.actor.connect('destroy', Lang.bind(this, + function() { + this.actor.destroy(); + })); + }, + + toggle: function() { + if (this._isOpen) + this.popdown(); + else + this.popup(); + }, + + popup: function() { + if (this._isOpen) + return; + + this.actor.show(); + this._boxPointer.setArrowOrigin(this._source.actor.x + this._source.actor.width / 2); + this._boxPointer.show(BoxPointer.PopupAnimation.FADE | + BoxPointer.PopupAnimation.SLIDE); + + this._isOpen = true; + this.emit('open-state-changed', true); + }, + + popdown: function() { + if (!this._isOpen) + return; + + this._boxPointer.hide(BoxPointer.PopupAnimation.FADE | + BoxPointer.PopupAnimation.SLIDE); + this._isOpen = false; + this.emit('open-state-changed', false); + } +}); +Signals.addSignalMethods(AppFolderPopup.prototype); const AppIcon = new Lang.Class({ Name: 'AppIcon', -- 2.39.5